/************************************************************************/
/* Some code parts are from the ASSIMP OpenGL sample                    */
/* Copyright (c) 2008-2010, ASSIMP Development Team                     */
/* http://assimp.sourceforge.net/index.html                             */
/************************************************************************/

#include "Model.h"

#include "../WindowsCompatibility/OpenGL.h"

#include <assimp/DefaultLogger.h>
#include <assimp/Logger.h>
#include "AssimpBypassLog.h"
#include <sys/stat.h>

Model::Model() {
	this->scene = NULL;
	this->isLoaded = false;
	this->log = NULL;
	this->modelparts = NULL;
}

Model::Model(Log *log) {
	this->scene = NULL;
	this->isLoaded = false;
	this->log = log;
	this->modelparts = NULL;	

	Assimp::DefaultLogger::create("",Assimp::Logger::NORMAL);
	//Assimp::DefaultLogger::get()->attachStream(new AssimpBypassLog(this->errLog, Assimp::Logger::DEBUGGING), Assimp::Logger::DEBUGGING);
	Assimp::DefaultLogger::get()->attachStream(new AssimpBypassLog(this->log, Assimp::Logger::INFO), Assimp::Logger::INFO);
	Assimp::DefaultLogger::get()->attachStream(new AssimpBypassLog(this->log, Assimp::Logger::WARN), Assimp::Logger::WARN);
	Assimp::DefaultLogger::get()->attachStream(new AssimpBypassLog(this->log, Assimp::Logger::ERR), Assimp::Logger::ERR);
}

Model::~Model() {
	if(this->scene != NULL)
		aiReleaseImport(this->scene);
	if(this->modelparts != NULL) {
		for(int i = 0; i < nbModelparts; i++) {
			delete modelparts[i];
		}
		delete[] this->modelparts;
		this->modelparts = NULL;
	}
	if(this->textures.size() != 0) {
		for(stdext::hash_map<std::string, DevilTexture*>::iterator it = textures.begin(); it != textures.end(); it++ )
		{
			delete ((DevilTexture*)it->second);
		}
	}
	this->log = NULL;
}

int Model::GetNbModelparts() {
	return this->nbModelparts;
}

Modelpart **Model::GetModelparts() {
	return this->modelparts;
}

const std::string &Model::GetModelFilePath() {
	return this->modelFilepath;
}

const std::string &Model::GetModelFileExtension() {
	return this->fileExtension;
}

int Model::Load(const char *filepath, float scalefactor, NxPhysicsSDK *sdk, ModelBoundingVolume::SHAPETYPE shapetype) {
	if(isLoaded == false) {
		if(this->log != NULL) {
			this->log->LogDebug(StringBuilder() << "Loading model (" << std::string(filepath) << ") ...");
		}

		std::string s = filepath;
		unsigned int lastSlashPos = s.find_last_of('\\');
		if(lastSlashPos == s.npos) lastSlashPos = s.find_last_of('/');
		this->modelFilepath = s.substr(0, lastSlashPos);
		this->fileExtension = s.substr(s.find_last_of('.'), s.length());
		this->scalefactor = scalefactor;

		//Check if file exists
		struct stat stFileInfo;
		bool fileExists;
		int intStat = stat(filepath, &stFileInfo);
		if(intStat == 0) fileExists = true;
		else			 fileExists = false;

		if(fileExists == false) {
			if(this->log != NULL)
				this->log->LogError(StringBuilder() << "File \"" << std::string(filepath) << "\" doesn't exists or isn't accessible!");
			return 1;
		}

		this->scene = aiImportFile(filepath,
			aiProcess_CalcTangentSpace       | 
			aiProcess_Triangulate            |
			aiProcess_JoinIdenticalVertices  |
			aiProcess_SortByPType);
		if (!scene) {
			if(this->log != NULL) {
				this->log->LogError("Unable to load imported scene!");
			}
			return 1;
		}

		GenerateDisplayLists(sdk, shapetype);
		
		this->isLoaded = true;
		return 0;
	} else {
		return 1;
	}	
}

int Model::LoadModelOnly(const char *filepath, float scalefactor) {
	if(isLoaded == false) {
		if(this->log != NULL) {
			this->log->LogDebug(StringBuilder() << "Loading model (" << std::string(filepath) << ") ...");
		}

		std::string s = filepath;
		this->modelFilepath = s.substr(0, s.find_last_of('\\'));
		this->fileExtension = s.substr(s.find_last_of('.'), s.length());
		this->scalefactor = scalefactor;

		//Check if file exists
		struct stat stFileInfo;
		bool fileExists;
		int intStat = stat(filepath, &stFileInfo);
		if(intStat == 0) fileExists = true;
		else			 fileExists = false;

		if(fileExists == false) {
			if(this->log != NULL) {
				this->log->LogError(StringBuilder() << "File \"" << std::string(filepath) << "\" doesn't exists or isn't accessible!");
			}
			return 1;
		}

		this->scene = aiImportFile(filepath,
			aiProcess_CalcTangentSpace       | 
			aiProcess_Triangulate            |
			aiProcess_JoinIdenticalVertices  |
			aiProcess_SortByPType);
		if (!scene) {
			if(this->log != NULL) {
				this->log->LogError("Unable to load imported scene!");
			}
			return 1;
		}

		GenerateDisplayLists(NULL, ModelBoundingVolume::BOX_SHAPE); //shapetype isnt used

		this->isLoaded = true;
		return 0;
	} else {
		return 1;
	}
}

void Model::Draw() {
	glCallList(this->completeModelDisplayList);
}

void Model::Color4ToFloat4(const aiColor4D *c, float f[4]) {
	f[0] = c->r;
	f[1] = c->g;
	f[2] = c->b;
	f[3] = c->a;
}

void Model::SetFloat4(float f[4], float a, float b, float c, float d) {
	f[0] = a;
	f[1] = b;
	f[2] = c;
	f[3] = d;
}

void Model::Color4f(const aiColor4D *color) {
	glColor4f(color->r, color->g, color->b, color->a);
}

void Model::ApplyMaterial(const aiMaterial *mtl) {
	float c[4];

	GLenum fill_mode;
	unsigned int ret1, ret2;
	struct aiColor4D diffuse;
	struct aiColor4D specular;
	struct aiColor4D ambient;
	struct aiColor4D emission;
	float shininess, strength;
	int two_sided;
	int wireframe;
	unsigned int max;

	SetFloat4(c, 0.8f, 0.8f, 0.8f, 1.0f);
	if(AI_SUCCESS == aiGetMaterialColor(mtl, AI_MATKEY_COLOR_DIFFUSE, &diffuse)) {
		Color4ToFloat4(&diffuse, c);
	}
	glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, c);

	SetFloat4(c, 0.2f, 0.2f, 0.2f, 1.0f);
	if(AI_SUCCESS == aiGetMaterialColor(mtl, AI_MATKEY_COLOR_AMBIENT, &ambient)) {
		Color4ToFloat4(&ambient, c);
	}
	glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, c);

	SetFloat4(c, 0.0f, 0.0f, 0.0f, 1.0f);
	if(AI_SUCCESS == aiGetMaterialColor(mtl, AI_MATKEY_COLOR_EMISSIVE, &emission)) {
		Color4ToFloat4(&emission, c);
	}
	glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, c);

	max = 1;
	ret1 = aiGetMaterialFloatArray(mtl, AI_MATKEY_SHININESS, &shininess, &max);
	max = 1;
	ret2 = aiGetMaterialFloatArray(mtl, AI_MATKEY_SHININESS_STRENGTH, &strength, &max);
	if((ret1 == AI_SUCCESS) && (ret2 == AI_SUCCESS)) {
		glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, shininess * strength);

		SetFloat4(c, 0.0f, 0.0f, 0.0f, 1.0f);
		if(AI_SUCCESS == aiGetMaterialColor(mtl, AI_MATKEY_COLOR_SPECULAR, &specular)) {
			Color4ToFloat4(&specular, c);
		}
		glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, c);
	} else {
		glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 0.0f);
		SetFloat4(c, 0.0f, 0.0f, 0.0f, 0.0f);
		glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, c);
	}

	max = 1;
	if(AI_SUCCESS == aiGetMaterialIntegerArray(mtl, AI_MATKEY_ENABLE_WIREFRAME, &wireframe, &max)) {
		fill_mode = wireframe ? GL_LINE : GL_FILL;
	} else {
		fill_mode = GL_FILL;
	}
	glPolygonMode(GL_FRONT_AND_BACK, fill_mode);

	max = 1;
	if((AI_SUCCESS == aiGetMaterialIntegerArray(mtl, AI_MATKEY_TWOSIDED, &two_sided, &max)) && two_sided) {
		glEnable(GL_CULL_FACE);
	} else {
		glDisable(GL_CULL_FACE);
	}
}

void Model::TraverseNodes(const aiScene *scene, const aiNode* node, Modelpart **modelpartsPointerArray, int &arrayIndex, unsigned int &displayListIndex, NxPhysicsSDK *sdk, ModelBoundingVolume::SHAPETYPE shapetype, aiMatrix4x4 parentTransformation) {
	aiMatrix4x4 completeNodeTransf;

	if(node == scene->mRootNode) {
		completeNodeTransf = node->mTransformation;
		parentTransformation = completeNodeTransf;
	} else {
		completeNodeTransf = node->mTransformation;
		aiMultiplyMatrix4(&completeNodeTransf,&parentTransformation);
		if(node->mNumChildren > 0) {
			aiMultiplyMatrix4(&parentTransformation,&(node->mTransformation));
		}
	}


	for (unsigned int n = 0; n < node->mNumMeshes; ++n) {
		const aiMesh* mesh = scene->mMeshes[node->mMeshes[n]];

		//create modelpart
		if(this->log != NULL) {
			modelpartsPointerArray[arrayIndex] = new Modelpart(log);
		} else {
			modelpartsPointerArray[arrayIndex] = new Modelpart();
		}

		//set name for modelpart
		if(node->mName.length > 0) {//valid name
			modelpartsPointerArray[arrayIndex]->SetName(node->mName.data);
		} else {
			modelpartsPointerArray[arrayIndex]->SetName(StringBuilder() << arrayIndex);
		}

		//create displaylist for modelpart
		ModelAiParamInfo maipi(scene, node, mesh, this->fileExtension);
		modelpartsPointerArray[arrayIndex]->SetDisplayListID(CreateDisplayListForMesh(maipi, displayListIndex));
		aiMatrix4x4 modelTMatrix = node->mTransformation;
		aiTransposeMatrix4(&modelTMatrix);
		modelpartsPointerArray[arrayIndex]->SetModelTransformationMatrix(modelTMatrix);
		
		//CREATE BOUNDING VOLUME (if needed)
		if(sdk != NULL) { //with BV
			ModelBoundingVolume *bv = NULL;
			if(this->log != NULL) {
				bv = new ModelBoundingVolume(sdk, log);
			} else {
				bv = new ModelBoundingVolume(sdk);
			}
			bv->CalcBoundingVolume(shapetype, maipi, completeNodeTransf, this->scalefactor);
			modelpartsPointerArray[arrayIndex]->SetBoundingVolume(bv);
		}

		arrayIndex++;
	}
		
	//traverse through all children-nodes
	for (unsigned int n = 0; n < node->mNumChildren; ++n) {
		TraverseNodes(scene, node->mChildren[n], modelpartsPointerArray, arrayIndex, displayListIndex, sdk, shapetype, parentTransformation);
	}

}

unsigned int Model::CreateDisplayListForMesh(const ModelAiParamInfo &modelAiInfo, unsigned int &displayListIndex) {
	const aiMesh *mesh = modelAiInfo.mesh;

	glNewList(displayListIndex, GL_COMPILE); //START LIST

	glPushMatrix();

	glScalef(this->scalefactor, this->scalefactor, this->scalefactor);

	if(mesh->HasNormals() == false) {
		glDisable(GL_LIGHTING);
	} else {
		glEnable(GL_LIGHTING);
	}

	if(mesh->HasVertexColors(0) == true) {
		glEnable(GL_COLOR_MATERIAL);
	} else {
		glDisable(GL_COLOR_MATERIAL);
	}

	//material
	aiMaterial *mat = scene->mMaterials[mesh->mMaterialIndex];
	ApplyMaterial(mat);		

	//texture
	if(mat->GetTextureCount(aiTextureType_NONE) == 0) {
		aiTextureType textureTypes[] = {aiTextureType_DIFFUSE, aiTextureType_SPECULAR, aiTextureType_AMBIENT, aiTextureType_EMISSIVE, aiTextureType_HEIGHT, aiTextureType_NORMALS
			,aiTextureType_SHININESS, aiTextureType_OPACITY, aiTextureType_DISPLACEMENT, aiTextureType_LIGHTMAP, aiTextureType_REFLECTION, aiTextureType_UNKNOWN};
		int numTextureTypes = 12;

		for(int texTypeIndex = 0; texTypeIndex < numTextureTypes; ++texTypeIndex) {
			int texCount = mat->GetTextureCount(textureTypes[texTypeIndex]);
			for(int texIndex = 0; texIndex < texCount; ++texIndex) {
				aiString texPath;
				aiTextureMapping *texMapping = NULL;
				unsigned int *uvIndex = 0;
				float *blend = NULL;
				aiTextureOp *texOp = NULL;
				aiTextureMapMode *texMapMode = NULL;

				if(mat->GetTexture(textureTypes[texTypeIndex], texIndex, &texPath, texMapping, uvIndex, blend, texOp, texMapMode) == aiReturn_SUCCESS) {
					std::string fullTexPath = this->modelFilepath;
					fullTexPath += "\\";
					fullTexPath += texPath.data;

					if(textures.count(fullTexPath) == 0) {
						textures[fullTexPath] = new DevilTexture();
					}
					if(textures[fullTexPath]->IsLoaded() == false) {
						textures[fullTexPath]->Load(fullTexPath.c_str(), true);
					}

					textures[fullTexPath]->Use();
				} else {
					if(this->log != NULL) {
						this->log->LogError("Texture cant be loaded.");
					}
				}
			}
		}
	}	

	//draw polygons
	for (unsigned int t = 0; t < mesh->mNumFaces; ++t) {
		const aiFace* face = &mesh->mFaces[t];
		GLenum face_mode;

		switch(face->mNumIndices) {
			case 1: face_mode = GL_POINTS; break;
			case 2: face_mode = GL_LINES; break;
			case 3: face_mode = GL_TRIANGLES; break;
			default: face_mode = GL_POLYGON; break;
		}

		glBegin(face_mode);
		for(unsigned int i = 0; i < face->mNumIndices; i++) {
			int index = face->mIndices[i];
			if(mesh->HasVertexColors(0) == true) {
				Color4f(&mesh->mColors[0][index]);
			}
			if(mesh->HasNormals() == true) {
				glNormal3fv(&mesh->mNormals[index].x);
			}
			if(mesh->HasTextureCoords(0) == true) {
				aiVector3D texCoord = mesh->mTextureCoords[0][index];
				glTexCoord2f(texCoord.x , 1-texCoord.y);
			}
			glVertex3fv(&mesh->mVertices[index].x);
		}
		glEnd();
	}

	glDisable(GL_TEXTURE_2D);

	glPopMatrix();

	glEndList();			 //END LIST

	return displayListIndex++;
}

void Model::GenerateDisplayLists(NxPhysicsSDK *sdk, ModelBoundingVolume::SHAPETYPE shapetype) {
	//prepare variables
	this->nbModelparts = this->scene->mNumMeshes;
	this->modelparts = new Modelpart*[nbModelparts];
	this->completeModelDisplayList = glGenLists(this->nbModelparts + 1);
	unsigned int displayListIndex = this->completeModelDisplayList + 1;
	int arrayIndex = 0;

	aiMatrix4x4 transformationMatrix;
	//traverse through nodes and create displaylists for each mesh and boundingvolumes
	TraverseNodes(this->scene, this->scene->mRootNode, this->modelparts, arrayIndex, displayListIndex, sdk, shapetype, transformationMatrix);

	glNewList(completeModelDisplayList, GL_COMPILE);	//START DISPLAYLIST

	//check environment opengl state
	GLboolean hadLight = 0;
	GLboolean hadColorMaterial = 0;
	glGetBooleanv(GL_LIGHTING, &hadLight);
	glGetBooleanv(GL_COLOR_MATERIAL, &hadColorMaterial);

	glPushMatrix();

	for(int i = 0; i < this->nbModelparts; ++i)	{
		glPushMatrix();
		glMultMatrixf((float*)&(this->modelparts[i]->GetModelTransformationMatrix()));
		this->modelparts[i]->Draw();
		glPopMatrix();
	}
	
	glPopMatrix();

	//reset environment opengl state
	if(hadLight) {
		glEnable(GL_LIGHTING);
	}
	if(hadColorMaterial) {
		glEnable(GL_COLOR_MATERIAL);
	}

	glEndList();										//END DISPLAYLIST
}
